package com.shuang.bai.simple.utils;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.apache.commons.lang3.tuple.Triple;

/**
 * a multiple string utils, can parse multiple line string starts and ends '''
 * 
 * @author Shuang Bai
 *
 */
public class MStringUtils {
	private static final Charset UTF8 = Charset.forName("utf-8");

	private static final String SEPARATOR = System.getProperty("line.separator");
	private static final String EMPTY_STRING = "";
	private static final String FOUR_BLANK = "    ";

	private static final Pattern PATTERN_COMMET_SINGLE_LINE = Pattern.compile("(?<!\\\\)//.*$", Pattern.MULTILINE);
	private static final Pattern PATTERN_COMMET_MULTI_LINE = Pattern.compile("(?<!\\\\)/\\*.*?\\*/",
			Pattern.DOTALL + Pattern.MULTILINE);

	private static final Pattern PATTERN_KEY_VALUE = Pattern.compile("^\\s*(\\S+)\\s*=\\s*'''(.*?[^\\\\])'''",
			Pattern.DOTALL + Pattern.MULTILINE);
	private static Pattern PATTERN_EXPRESSION = Pattern
			.compile("(?<!\\\\)\\$(\\{\\s*(\\S+)\\s*\\}|([^\\{\\s\\$]\\S*\\b))");
	private static final Pattern PATTERN_ESCAPE = Pattern.compile("\\\\(?='''|//|\\$(\\{\\s*\\S+\\s*\\}|[^\\s\\{]))");

	private static final Pattern PATTERN_STRIP = Pattern.compile("^( +)", Pattern.MULTILINE);

	private MStringUtils() {
	}

	/**
	 * use template to get key value, the value is multiple String, multiple
	 * string include '''content'''. support commnet: // this comment support
	 * expressions: this is $name or ${name} also can escape: \// this is not
	 * comment, \$name or \${name} are not expression
	 * 
	 * @param templateURI
	 *            template file resource
	 * @param supplement
	 *            beside template key value parse together
	 * @param bindings
	 *            binding data for expression
	 * @return paresed key and value
	 */
	private static Map<String, String> getMultipleString(URI templateURI, Map<String, String> supplement,
			Map<String, String> bindings) {
		Validate.notNull(templateURI, "templateURI");
		Validate.notNull(bindings, "bindings");
		Validate.notNull(supplement, "supplement");

		String rawContent = readRawString(templateURI);
		Map<String, String> keyValue = parseKeyValue(rawContent);
		keyValue.putAll(supplement);

		Map<String, String> bindedKeyValue = bindVariable(keyValue, bindings);

		Map<String, String> parsedKeyValue = parseAllExpression(bindedKeyValue);
		Map<String, String> trimedKeyValue = trimAndRemoveEscape(parsedKeyValue);
		return trimedKeyValue;
	}

	/**
	 * parse expressions, with bindings and keyValues itself. 
	 * 
	 * @param keyValues
	 *            key value will be parsed
	 * @param bindings
	 *            binding data for expression
	 * @return paresed key and value
	 */
	private static Map<String, String> getMultipleString(Map<String, String> keyValues, Map<String, String> bindings) {
		Validate.notNull(bindings, "bindings");
		Validate.notNull(keyValues, "supplement");

		Map<String, String> bindedKeyValue = bindVariable(keyValues, bindings);

		Map<String, String> parsedKeyValue = parseAllExpression(bindedKeyValue);
		Map<String, String> trimedKeyValue = trimAndRemoveEscape(parsedKeyValue);
		return trimedKeyValue;
	}

	/**
	 * 
	 * @param templateURI
	 *            template file uri
	 * @param bindings
	 *            binding data for expression
	 * @see #getMultipleString(URI,Map)
	 */
	public static Map<String, String> getMultipleString(URI templateURI, Map<String, String> bindings) {
		Validate.notNull(templateURI, "templateURI");
		Validate.notNull(bindings, "bindings");

		return getMultipleString(templateURI, bindings, Collections.emptyMap());
	}

	/**
	 * all the value have not expression, so you can not supply bindings
	 * 
	 * @param templateURI
	 *            template file uri
	 * @see #getMultipleString(URI,Map)
	 */
	public static Map<String, String> getMultipleString(URI templateURI) {
		Validate.notNull(templateURI, "templateURI");
		return getMultipleString(templateURI, Collections.emptyMap());
	}

	/**
	 * 
	 * @param templatePath
	 *            template file path
	 * @param bindings
	 *            binding data for expression
	 * @see #getMultipleString(Path,Map)
	 */
	public static Map<String, String> getMultipleString(Path templatePath, Map<String, String> bindings) {
		Validate.notNull(templatePath, "templatePath");
		Validate.notNull(bindings, "bindings");

		return getMultipleString(templatePath.toUri(), bindings);
	}

	/**
	 * every value have no expression, so this method not need supply bindings
	 * 
	 * @param templatePath
	 *            template file path
	 * @param bindings
	 *            binding data for expression
	 * @see #getMultipleString(Path,Map)
	 */
	public static Map<String, String> getMultipleString(Path templatePath) {
		Validate.notNull(templatePath, "templatePath");

		return getMultipleString(templatePath.toUri(), Collections.<String, String>emptyMap());
	}

	/**
	 * fill the object not final String with template please call this method in
	 * constructor, and after init other String fields.
	 * 
	 * @param obj
	 */
	public static void fill(Object obj) {
		fill(obj, Collections.emptyMap());
	}

	/**
	 * fill the object not final String with template please call this method in
	 * constructor, and after init other String fields.
	 * 
	 * @param obj
	 * @param moreBindings constant key value for fill expressions: e.g hello ${name}.  moreBindings[name=Shuang]
	 */
	public static void fill(Object obj, Map<String, String> moreBindings) {
		Validate.notNull(obj, "can't fill multiple string to null");
		Validate.notNull(moreBindings);
		Class<?> clazz = obj.getClass();
		URL templateURL = clazz.getResource("/" + clazz.getName().replace('.', '/') + ".tmp");

		URI templateUri = null;
		if (templateURL != null) {
			try {
				templateUri = templateURL.toURI();
			} catch (URISyntaxException e) {
				// impossible
				throw new RuntimeException("convert url to uri fail", e);
			}
		}
		Class<? extends Object> currentClass = clazz;
		Map<String, String> bindings = new HashMap<>(moreBindings);
		Map<String, String> supplement = new HashMap<>();
		List<Field> needWriteFields = new ArrayList<>();
		do {
			Field[] fields = currentClass.getDeclaredFields();
			for (Field field : fields) {

				if (field.getType() != String.class) {
					continue;
				}
				boolean isStaticFinal = Modifier.isStatic(field.getModifiers())
						&& Modifier.isFinal(field.getModifiers());
				String value = null;
				try {
					value = (String) FieldUtils.readField(field, obj, true);
				} catch (IllegalAccessException e) {
					throw new RuntimeException("read field value by reflection fail, this field is " + field, e);
				}
				if (Objects.isNull(value)) {
					if (field.getDeclaringClass() == clazz) {
						needWriteFields.add(field);
					}
					continue;
				}
				String fieldName = field.getName();

				if (field.getDeclaringClass() == clazz && !isStaticFinal) {
					supplement.put(fieldName, value);
					if (hasExpression(value)) {
						needWriteFields.add(field);
					}
				} else if ((currentClass != clazz || isStaticFinal) && !bindings.containsKey(fieldName)) {
					bindings.put(fieldName, value);
				}
			}

		} while ((currentClass = currentClass.getSuperclass()) != Object.class);

		Map<String, String> multipleString = (templateUri == null) ? getMultipleString(supplement, bindings)
				: getMultipleString(templateUri, supplement, bindings);
		setValueForField(obj, needWriteFields, multipleString);
	}

	/**
	 * fill static String value with template
	 * 
	 * @param clazz
	 * @param moreBindings constant key value for fill expressions: e.g hello ${name}.  moreBindings[name=Shuang]
	 */
	private static void fillStatic(Class<?> clazz, Map<String, String> moreBindings) {
		Validate.notNull(clazz, "can't fill multiple string to null");
		Validate.notNull(moreBindings);
		URL templateURL = clazz.getResource("/" + clazz.getName().replace('.', '/') + ".tmp");
		Validate.notNull(templateURL, "template file: %s not exists", clazz.getName().replace('.', '/') + ".tmp");
		URI templateUri = null;
		if (templateURL != null) {
			try {
				templateUri = templateURL.toURI();
			} catch (URISyntaxException e) {
				// impossible
				throw new RuntimeException("convert url to uri fail", e);
			}
		}

		Class<? extends Object> currentClass = clazz;
		Map<String, String> bindings = new HashMap<>(moreBindings);
		Map<String, String> supplement = new HashMap<>();
		List<Field> needWriteFields = new ArrayList<>();

		do {
			Field[] fields = currentClass.getDeclaredFields();
			for (Field field : fields) {
				if (field.getType() != String.class || !Modifier.isStatic(field.getModifiers())) {
					continue;
				}
				boolean isFinal = Modifier.isFinal(field.getModifiers());
				String value = null;
				try {
					value = (String) FieldUtils.readStaticField(field, true);
				} catch (IllegalAccessException e) {
					throw new RuntimeException("read field value by reflection fail, this field is " + field, e);
				}
				if (Objects.isNull(value)) {
					if (field.getDeclaringClass() == clazz) {
						needWriteFields.add(field);
					}
					continue;
				}
				String fieldName = field.getName();

				if (field.getDeclaringClass() == clazz && !isFinal) {
					supplement.put(fieldName, value);
					if (hasExpression(value)) {
						needWriteFields.add(field);
					}
				} else if ((currentClass != clazz || isFinal) && !bindings.containsKey(fieldName)) {
					bindings.put(fieldName, value);
				}
			}

		} while ((currentClass = currentClass.getSuperclass()) != Object.class);

		Map<String, String> multipleString = (templateUri == null) ? getMultipleString(supplement, bindings)
				: getMultipleString(templateUri, supplement, bindings);
		setValueForField(null, needWriteFields, multipleString);
	}

	/**
	 * fill static String value with template, the fill class is the caller
	 * class, please call this method in static block
	 */
	public static void fillStatic() {
		StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
		StackTraceElement caller = stackTrace[2];
		try {
			Class<?> callerClazz = Class.forName(caller.getClassName());
			fillStatic(callerClazz, Collections.emptyMap());
		} catch (ClassNotFoundException e) {
			// impossible
			throw new RuntimeException(e);
		}
	}

	/**
	 * fill static String value with template, the fill class is the caller
	 * class, please call this method in static block
	 * @param bindings constant key value for fill expressions: e.g hello ${name}.  moreBindings[name=Shuang]
	 */
	public static void fillStatic(Map<String, String> bindings) {
		StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
		StackTraceElement caller = stackTrace[2];
		try {
			Class<?> callerClazz = Class.forName(caller.getClassName());
			fillStatic(callerClazz, bindings);
		} catch (ClassNotFoundException e) {
			// impossible
			throw new RuntimeException(e);
		}
	}

	private static Map<String, String> trimAndRemoveEscape(Map<String, String> keyValue) {
		Map<String, String> result = new HashMap<>();
		for (Entry<String, String> pair : keyValue.entrySet()) {
			String value = pair.getValue();
			String removeEscape = PATTERN_ESCAPE.matcher(value).replaceAll(EMPTY_STRING);
			result.put(pair.getKey(), removeEscape);
		}
		return result;
	}

	private static String readRawString(URI templateURI) {
		Validate.notNull(templateURI, "templatePath");
		StringBuilder noSingleLineComment = new StringBuilder();
		try (BufferedReader reader = new BufferedReader(
				new InputStreamReader(templateURI.toURL().openStream(), UTF8))) {
			String line = null;
			while ((line = reader.readLine()) != null) {
				String removeComment = PATTERN_COMMET_SINGLE_LINE.matcher(line).replaceAll(EMPTY_STRING);
				if (StringUtils.isNotBlank(removeComment)) {
					String replaceTab = removeComment.replace("\t", FOUR_BLANK);
					noSingleLineComment.append(replaceTab).append(SEPARATOR);
				}
			}
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
		String noMultiLineComment = PATTERN_COMMET_MULTI_LINE.matcher(noSingleLineComment.toString())
				.replaceAll(EMPTY_STRING);
		return noMultiLineComment;
	}

	/**
	 * bind variable with expression, e.g: $name or ${name }
	 * 
	 * @param keyValue
	 *            have expression string content
	 * @param bindings
	 *            variables
	 * @return binded key value
	 */
	private static Map<String, String> bindVariable(Map<String, String> keyValue, Map<String, String> bindings) {
		Validate.notNull(keyValue, "content");
		Validate.notNull(bindings, "bindings");
		Map<String, String> bindedKeyValue = new HashMap<>();
		for (Entry<String, String> pair : keyValue.entrySet()) {
			String key = pair.getKey();
			String value = pair.getValue();
			String bindedValue = bindExpression(value, bindings);
			bindedKeyValue.put(key, bindedValue);
		}
		return bindedKeyValue;
	}

	/**
	 * bind expression values for a String object, for example: hello $name, good morning.
	 * @param value need fill expression value
	 * @param bindings key value pair for binding
	 * @return binded value
	 */
	private static String bindExpression(String value, Map<String, String> bindings) {
		Matcher matcher = PATTERN_EXPRESSION.matcher(value);
		if (!matcher.find()) {
			return value;
		} else {
			// bind param into content, use expression: e.g: $name or ${name }
			StringBuilder parsedValue = new StringBuilder();
			List<Triple<Integer, Integer, String>> marks = new ArrayList<>();
			do {
				String expressionKey = ObjectUtils.defaultIfNull(matcher.group(2), matcher.group(3));
				String expressionValue = bindings.get(expressionKey);
				if (expressionValue != null) {
					int start = matcher.start();
					int end = matcher.end();
					marks.add(Triple.of(start, end, expressionValue));
				}
			} while (matcher.find());
			int lastEnd = 0;
			for (Triple<Integer, Integer, String> triple : marks) {
				parsedValue.append(value.substring(lastEnd, triple.getLeft())).append(triple.getRight());
				lastEnd = triple.getMiddle();
			}
			parsedValue.append(value.substring(lastEnd));
			return parsedValue.toString();
		}
	}

	/**
	 * whole content parse to key value
	 * 
	 * @param bindedContent
	 *            content
	 * @return key and value
	 */
	private static Map<String, String> parseKeyValue(String bindedContent) {
		Validate.notNull(bindedContent, "bindedContent");
		Map<String, String> keyValues = new HashMap<>();
		Matcher matcher = PATTERN_KEY_VALUE.matcher(bindedContent);
		while (matcher.find()) {
			String key = matcher.group(1);
			String value = matcher.group(2);
			String striped = stripStr(value);
			keyValues.put(key, striped);
		}
		return keyValues;
	}

	/**
	 * strip multiple line string left space, make the string left align
	 * 
	 * @param str
	 * @return
	 */
	private static String stripStr(String str) {
		String[] split = StringUtils.split(str, "\n\r\n");
		if (split.length == 0) {
			return str;
		} else if (split.length == 1) {
			return split[0];
		}
		split[0] = PATTERN_STRIP.matcher(split[0]).replaceFirst(EMPTY_STRING);
		int minLengthOfSpace = Integer.MAX_VALUE;
		for (int i = 1; i < split.length; i++) {
			Matcher matcher = PATTERN_STRIP.matcher(split[i]);
			if (!matcher.find()) {
				return str;
			}
			String group = matcher.group(1);
			minLengthOfSpace = Math.min(minLengthOfSpace, group.length());
		}
		split[0] = StringUtils.leftPad(split[0], minLengthOfSpace, ' ');
		for (int i = 1; i < split.length; i++) {
			split[i] = split[i].substring(minLengthOfSpace);
		}
		return StringUtils.join(split, '\n');
	}

	/**
	 * parse all the expressions in the value
	 * 
	 * @param needParseKeyValue
	 *            need pares key value map
	 * @return parsed key value
	 */
	private static Map<String, String> parseAllExpression(Map<String, String> needParseKeyValue) {
		Validate.notNull(needParseKeyValue, "needParseKeyValue");
		Map<String, String> completed = new HashMap<>();
		Map<String, String> notCompleted = new HashMap<>();
		for (Entry<String, String> pair : needParseKeyValue.entrySet()) {
			String key = pair.getKey();
			String value = pair.getValue();
			if (hasExpression(value)) {
				notCompleted.put(key, value);
			} else {
				completed.put(key, value);
			}
		}
		while (MapUtils.isNotEmpty(notCompleted)) {
			Map<String, String> hasExp = new HashMap<>();

			for (Entry<String, String> kv : notCompleted.entrySet()) {
				String bindedValue = bindExpression(kv.getValue(), completed);
				if (hasExpression(bindedValue)) {
					hasExp.put(kv.getKey(), bindedValue);
				} else {
					completed.put(kv.getKey(), bindedValue);
				}
			}
			// not compleate anyone, so has can't complete expression
			if (notCompleted.size() == hasExp.size()) {
				StringBuilder sb = new StringBuilder(
						"parse expression fails, expressions can't find value or contains cycle denpendency:");
				for (Entry<String, String> pair : hasExp.entrySet()) {
					sb.append('\n').append(pair.getKey()).append("=").append('\n').append(pair.getValue());
				}
				throw new RuntimeException(sb.toString());
			}
			notCompleted = hasExp;
		}

		return completed;
	}

	/**
	 * check if the String contains expression
	 * @param value be checked String object
	 * @return true if have expression, false if not.
	 */
	private static boolean hasExpression(String value) {
		Validate.notNull(value, "value");
		return PATTERN_EXPRESSION.matcher(value).find();
	}

	/**
	 * set Field value for an object, if obj is null, then set static field value.
	 * @param obj fields belong to this object
	 * @param fields need set fields 
	 * @param multipleString field name and value map
	 */
	private static void setValueForField(Object obj, List<Field> fields, Map<String, String> multipleString) {
		Validate.notNull(fields, "fields");
		Validate.notNull(multipleString, "multipleString");
		for (Field field : fields) {
			String value = multipleString.get(field.getName());
			try {
				FieldUtils.writeField(field, obj, value, true);
			} catch (IllegalAccessException e) {
				// impossible
				throw new RuntimeException(
						"write field value fail, the field is " + field.getName() + ", value is " + value, e);
			}
		}
	}
}
